package com.gavin.utils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;

final public class XMLElement {
	static long serialVersionUID = 6685035139346394777L;

	public static int NANOXML_MAJOR_VERSION = 2;

	public static int NANOXML_MINOR_VERSION = 2;

	private HashMap<String, String> attributes;

	private ArrayList<XMLElement> children;

	private String name;

	private String contents;

	private int lineNr;

	private boolean ignoreCase;

	private boolean ignoreWhitespace;

	private char charReadTooMuch;

	char[] chsReader;
	int nIndex = 0;

	public XMLElement() {
		this(false, true, true);
	}

	public XMLElement(HashMap<String, String> entities) {
		this(false, true, true);
	}

	public XMLElement(boolean skipLeadingWhitespace) {
		this(skipLeadingWhitespace, true, true);
	}

	protected XMLElement(boolean skipLeadingWhitespace, boolean fillBasicConversionTable, boolean ignoreCase) {
		this.ignoreWhitespace = skipLeadingWhitespace;
		this.ignoreCase = ignoreCase;
		this.name = null;
		this.contents = "";
		this.attributes = new HashMap<String, String>();
		this.children = new ArrayList<XMLElement>();
		this.lineNr = 0;
	}

	public void addChild(XMLElement child) {
		this.children.add(child);
	}

	public void setAttribute(String name, Object value) {
		if (this.ignoreCase) {
			name = name.toUpperCase();
		}
		this.attributes.put(name, value.toString());
	}

	public int countChildren() {
		return this.children.size();
	}

	public Set<String> enumerateAttributeNames() {
		return this.attributes.keySet();
	}

	public Iterator<XMLElement> enumerateChildren() {
		return this.children.iterator();
	}

	public ArrayList<XMLElement> getChildren() {
		return this.children;
	}

	public String getContents() {
		return this.getContent();
	}

	public String getContent() {
		return this.contents;
	}

	public int getLineNr() {
		return this.lineNr;
	}

	public Object getAttribute(String name) {
		return this.getAttribute(name, null);
	}

	public Object getAttribute(String name, Object defaultValue) {
		if (this.ignoreCase) {
			name = name.toUpperCase();
		}
		Object value = this.attributes.get(name);
		if (value == null) {
			value = defaultValue;
		}
		return value;
	}


	public String getStringAttribute(String name) {
		return this.getStringAttribute(name, null);
	}

	public String getStringAttribute(String name, String defaultValue) {
		return (String) this.getAttribute(name, defaultValue);
	}

	public String getName() {
		return this.name;
	}

	public String getTagName() {
		return this.getName();
	}

	public void parseFromReader() throws IOException, XMLException {
		this.parseFromReader(/* startingLineNr */1);
	}

	public void parseFromReader(int startingLineNr) throws XMLException {
		this.name = null;
		this.contents = "";
		this.attributes = new HashMap<String, String>();
		this.children = new ArrayList<XMLElement>();
		this.charReadTooMuch = '\0';

		while (true) {
			char ch = this.scanWhitespace();

			if (ch != '<') {
				throw this.expectedInput("<");
			}

			ch = this.readChar();

			if ((ch == '!') || (ch == '?')) {
				this.skipSpecialTag(0);
			} else {
				this.charReadTooMuch = ch;
				this.scanElement(this);
				return;
			}
		}
	}

	public void parseString(String string) throws XMLException {
		this.chsReader = string.toCharArray();
		this.parseFromReader(/* startingLineNr */1);
	}

	public void parseString(String string, int offset) throws XMLException {
		this.parseString(string.substring(offset));
	}

	public void parseString(String string, int offset, int end) throws XMLException {
		this.parseString(string.substring(offset, end));
	}

	public void parseCharArray(char[] input, int offset, int end) throws XMLException {
		this.parseCharArray(input, offset, end, /* startingLineNr */1);
	}

	public void parseCharArray(char[] input, int offset, int end, int startingLineNr) throws XMLException {
		this.parseFromReader(startingLineNr);
	}

	public void removeChild(XMLElement child) {
		this.children.remove(child);
	}

	public void removeAttribute(String name) {
		if (this.ignoreCase) {
			name = name.toUpperCase();
		}
		this.attributes.remove(name);
	}

	public void removeChild(String name) {
		this.removeAttribute(name);
	}

	protected XMLElement createAnotherElement() {
		return new XMLElement(this.ignoreWhitespace, false, this.ignoreCase);
	}

	public void setContent(String content) {
		this.contents = content;
	}

	public void setTagName(String name) {
		this.setName(name);
	}

	public void setName(String name) {
		this.name = name;
	}

	public String toString() {
		try {
			ByteArrayOutputStream out = new ByteArrayOutputStream();
			OutputStreamWriter writer = new OutputStreamWriter(out);
			this.write(writer);
			writer.flush();
			return new String(out.toByteArray());
		} catch (IOException e) {
			// Java exception handling suxx
			return super.toString();
		}
	}

	public void write(Writer writer) throws IOException {
		if (this.name == null) {
			this.writeEncoded(writer, this.contents);
			return;
		}
		writer.write('<');
		writer.write(this.name);
		if (!this.attributes.isEmpty()) {
			Set<String> enum1 = this.attributes.keySet();
			Iterator<String> iter = enum1.iterator();
			while (iter.hasNext()) {
				writer.write(' ');
				String key = (String) iter.next();
				String value = (String) this.attributes.get(key);
				writer.write(key);
				writer.write('=');
				writer.write('"');
				this.writeEncoded(writer, value);
				writer.write('"');
			}
		}
		if ((this.contents != null) && (this.contents.length() > 0)) {
			writer.write('>');
			this.writeEncoded(writer, this.contents);
			writer.write('<');
			writer.write('/');
			writer.write(this.name);
			writer.write('>');
		} else if (this.children.isEmpty()) {
			writer.write('/');
			writer.write('>');
		} else {
			writer.write('>');
			Iterator<XMLElement> enum1 = this.enumerateChildren();
			while (enum1.hasNext()) {
				XMLElement child = (XMLElement) enum1.next();
				child.write(writer);
			}
			writer.write('<');
			writer.write('/');
			writer.write(this.name);
			writer.write('>');
		}
	}

	protected void writeEncoded(Writer writer, String str) throws IOException {
		for (int i = 0; i < str.length(); i += 1) {
			char ch = str.charAt(i);
			switch (ch) {
			case '<':
				writer.write('&');
				writer.write('l');
				writer.write('t');
				writer.write(';');
				break;
			case '>':
				writer.write('&');
				writer.write('g');
				writer.write('t');
				writer.write(';');
				break;
			case '&':
				writer.write('&');
				writer.write('a');
				writer.write('m');
				writer.write('p');
				writer.write(';');
				break;
			case '"':
				writer.write('&');
				writer.write('q');
				writer.write('u');
				writer.write('o');
				writer.write('t');
				writer.write(';');
				break;
			case '\'':
				writer.write('&');
				writer.write('a');
				writer.write('p');
				writer.write('o');
				writer.write('s');
				writer.write(';');
				break;
			default:
				int unicode = (int) ch;
				if ((unicode < 32) || (unicode > 126)) {
					writer.write('&');
					writer.write('#');
					writer.write('x');
					writer.write(Integer.toString(unicode, 16));
					writer.write(';');
				} else {
					writer.write(ch);
				}
			}
		}
	}

	private void scanIdentifier(StringBuilder result) {
		char ch;
		while (true) {
			if (charReadTooMuch != '\0') {
				ch = this.charReadTooMuch;
				this.charReadTooMuch = '\0';
			} else {
				ch = this.chsReader[nIndex++];
			}

			if (((ch < 'A') || (ch > 'Z')) && ((ch < 'a') || (ch > 'z')) && ((ch < '0') || (ch > '9')) && (ch != '_')
					&& (ch != '.') && (ch != ':') && (ch != '-') && (ch <= '\u007E')) {
				this.charReadTooMuch = ch;
				return;
			}
			result.append(ch);
		}
	}

	private char scanWhitespace() {
		char ch;

		if (charReadTooMuch != '\0') {
			ch = this.charReadTooMuch;
			this.charReadTooMuch = '\0';
			switch (ch) {
			case ' ':
			case '\t':
			case '\n':
			case '\r':
				break;
			default:
				return ch;
			}
		}

		while (true) {

			ch = this.chsReader[nIndex++];
			switch (ch) {
			case ' ':
			case '\t':
			case '\n':
			case '\r':
				break;
			default:
				return ch;
			}
		}
	}

	private char scanWhitespace(StringBuilder result) {
		while (true) {
			char ch = this.readChar();
			switch (ch) {
			case ' ':
			case '\t':
			case '\n':
				result.append(ch);
			case '\r':
				break;
			default:
				return ch;
			}
		}
	}

	private char[] chs = new char[100];

	private void scanString(StringBuilder string) {
		char delimiter = this.readChar();
		if ((delimiter != '\'') && (delimiter != '"')) {
			throw this.expectedInput("' or \"");
		}

		int nIndexString = 0;
		while (true) {
			char ch = this.readChar();
			if (ch == delimiter) {
				break;
			} else if (ch == '&') {
				if (nIndexString > 0) {
					string.append(chs, 0, nIndexString);
					nIndexString = 0;
				}
				this.resolveEntity(string);
			} else {
				chs[nIndexString] = ch;
				nIndexString++;
				if (nIndexString >= chs.length) {
					string.append(chs, 0, nIndexString);
					nIndexString = 0;
				}
			}
		}
		if (nIndexString > 0) {
			string.append(chs, 0, nIndexString);
			nIndexString = 0;
		}
	}

	private void scanPCData(StringBuilder data) {
		for (;;) {
			char ch = this.readChar();
			if (ch == '<') {
				ch = this.readChar();
				if (ch == '!') {
					this.checkCDATA(data);
				} else {
					this.charReadTooMuch = ch;
					return;
				}
			} else if (ch == '&') {
				this.resolveEntity(data);
			} else {
				data.append(ch);
			}
		}
	}

	private boolean checkCDATA(StringBuilder buf) {
		char ch = this.readChar();
		if (ch != '[') {
			this.charReadTooMuch = ch;
			this.skipSpecialTag(0);
			return false;
		} else if (!this.checkLiteral("CDATA[")) {
			this.skipSpecialTag(1); // one [ has already been read
			return false;
		} else {
			int delimiterCharsSkipped = 0;
			while (delimiterCharsSkipped < 3) {
				ch = this.readChar();
				switch (ch) {
				case ']':
					if (delimiterCharsSkipped < 2) {
						delimiterCharsSkipped += 1;
					} else {
						buf.append(']');
						buf.append(']');
						delimiterCharsSkipped = 0;
					}
					break;
				case '>':
					if (delimiterCharsSkipped < 2) {
						for (int i = 0; i < delimiterCharsSkipped; i++) {
							buf.append(']');
						}
						delimiterCharsSkipped = 0;
						buf.append('>');
					} else {
						delimiterCharsSkipped = 3;
					}
					break;
				default:
					for (int i = 0; i < delimiterCharsSkipped; i += 1) {
						buf.append(']');
					}
					buf.append(ch);
					delimiterCharsSkipped = 0;
				}
			}
			return true;
		}
	}

	private void skipComment() {
		int dashesToRead = 2;
		while (dashesToRead > 0) {
			char ch = this.readChar();
			if (ch == '-') {
				dashesToRead -= 1;
			} else {
				dashesToRead = 2;
			}
		}
		if (this.readChar() != '>') {
			throw this.expectedInput(">");
		}
	}

	private void skipSpecialTag(int bracketLevel) {
		int tagLevel = 1; // <
		char stringDelimiter = '\0';
		if (bracketLevel == 0) {
			char ch = this.readChar();
			if (ch == '[') {
				bracketLevel += 1;
			} else if (ch == '-') {
				ch = this.readChar();
				if (ch == '[') {
					bracketLevel += 1;
				} else if (ch == ']') {
					bracketLevel -= 1;
				} else if (ch == '-') {
					this.skipComment();
					return;
				}
			}
		}
		while (tagLevel > 0) {
			char ch = this.readChar();
			if (stringDelimiter == '\0') {
				if ((ch == '"') || (ch == '\'')) {
					stringDelimiter = ch;
				} else if (bracketLevel <= 0) {
					if (ch == '<') {
						tagLevel += 1;
					} else if (ch == '>') {
						tagLevel -= 1;
					}
				}
				if (ch == '[') {
					bracketLevel += 1;
				} else if (ch == ']') {
					bracketLevel -= 1;
				}
			} else {
				if (ch == stringDelimiter) {
					stringDelimiter = '\0';
				}
			}
		}
	}

	private boolean checkLiteral(String literal) {
		int length = literal.length();
		for (int i = 0; i < length; i += 1) {
			if (this.readChar() != literal.charAt(i)) {
				return false;
			}
		}
		return true;
	}

	private char readChar() {
		if (charReadTooMuch != '\0') {
			char ch = this.charReadTooMuch;
			this.charReadTooMuch = '\0';
			return ch;
		}

		return this.chsReader[nIndex++];
	}

	private void scanElement(XMLElement elt) {
		StringBuilder buf = new StringBuilder();
		this.scanIdentifier(buf);
		String name = buf.toString();
		elt.setName(name);
		char ch = this.scanWhitespace();
		while ((ch != '>') && (ch != '/')) {
			buf.setLength(0);
			this.charReadTooMuch = ch;
			this.scanIdentifier(buf);
			String key = buf.toString();
			ch = this.scanWhitespace();
			if (ch != '=') {
				throw this.expectedInput("=");
			}
			this.charReadTooMuch = this.scanWhitespace();
			buf.setLength(0);
			this.scanString(buf);
			elt.setAttribute(key, buf);
			ch = this.scanWhitespace();
		}
		if (ch == '/') {
			ch = this.readChar();
			if (ch != '>') {
				throw this.expectedInput(">");
			}
			return;
		}
		buf.setLength(0);
		ch = this.scanWhitespace(buf);
		if (ch != '<') {
			this.charReadTooMuch = ch;
			this.scanPCData(buf);
		} else {
			while (true) {
				ch = this.readChar();
				if (ch == '!') {
					if (this.checkCDATA(buf)) {
						this.scanPCData(buf);
						break;
					} else {
						ch = this.scanWhitespace(buf);
						if (ch != '<') {
							this.charReadTooMuch = ch;
							this.scanPCData(buf);
							break;
						}
					}
				} else {
					if ((ch != '/') || this.ignoreWhitespace) {
						buf.setLength(0);
					}
					if (ch == '/') {
						this.charReadTooMuch = ch;
					}
					break;
				}
			}
		}
		if (buf.length() == 0) {
			while (ch != '/') {
				if (ch == '!') {
					ch = this.readChar();
					if (ch != '-') {
						throw this.expectedInput("Comment or Element");
					}
					ch = this.readChar();
					if (ch != '-') {
						throw this.expectedInput("Comment or Element");
					}
					this.skipComment();
				} else {
					this.charReadTooMuch = ch;
					XMLElement child = this.createAnotherElement();
					this.scanElement(child);
					elt.addChild(child);
				}
				ch = this.scanWhitespace();
				if (ch != '<') {
					throw this.expectedInput("<");
				}
				ch = this.readChar();
			}
			this.charReadTooMuch = ch;
		} else {
			if (this.ignoreWhitespace) {
				elt.setContent(buf.toString().trim());
			} else {
				elt.setContent(buf.toString());
			}
		}
		ch = this.readChar();
		if (ch != '/') {
			throw this.expectedInput("/");
		}
		this.charReadTooMuch = this.scanWhitespace();
		if (!this.checkLiteral(name)) {
			throw this.expectedInput(name);
		}
		if (this.scanWhitespace() != '>') {
			throw this.expectedInput(">");
		}
	}

	private char[] chsKeyBuffer = new char[10];

	private void resolveEntity(StringBuilder buf) {
		char ch = '\0';
		int nIndexKey = 0;
		while (true) {
			ch = this.readChar();
			if (ch == ';') {
				break;
			}
			this.chsKeyBuffer[nIndexKey] = ch;
			nIndexKey++;
			if (nIndexKey >= this.chsKeyBuffer.length) {
				throw this.unknownEntity(new String(this.chsKeyBuffer));
			}
		}
		String key = new String(this.chsKeyBuffer, 0, nIndexKey);
		if (this.chsKeyBuffer[0] == '#') {
			try {
				if (key.charAt(1) == 'x') {
					ch = (char) Integer.parseInt(key.substring(2), 16);
				} else {
					ch = (char) Integer.parseInt(key.substring(1), 10);
				}
			} catch (NumberFormatException e) {
				throw this.unknownEntity(key);
			}
			buf.append(ch);
		} else {
			if (key.equals("amp")) {
				buf.append('&');
			} else if (key.equals("quot")) {
				buf.append('"');
			} else if (key.equals("apos")) {
				buf.append('\'');
			} else if (key.equals("lt")) {
				buf.append('<');
			} else if (key.equals("gt")) {
				buf.append('>');
			} else {
				throw this.unknownEntity(key);
			}
		}
	}

	private XMLException expectedInput(String charSet) {
		String msg = "Expected: " + charSet;
		return new XMLException(this.getName(), msg);
	}

	private XMLException unknownEntity(String name) {
		String msg = "Unknown or invalid entity: &" + name + ";";
		return new XMLException(this.getName(), msg);
	}

	public XMLElement getChildAt(int index) {
		return (XMLElement) this.children.get(index);
	}
	

//	public Object getAttribute(String name, HashMap valueSet, String defaultKey, boolean allowLiterals) {
//		if (this.ignoreCase) {
//			name = name.toUpperCase();
//		}
//		Object key = this.attributes.get(name);
//		Object result;
//		if (key == null) {
//			key = defaultKey;
//		}
//		result = valueSet.get(key);
//		if (result == null) {
//			if (allowLiterals) {
//				result = key;
//			} else {
//				throw this.invalidValue(name, (String) key);
//			}
//		}
//		return result;
//	}	
//	public String getStringAttribute(String name, HashMap valueSet, String defaultKey, boolean allowLiterals) {
//		return (String) this.getAttribute(name, valueSet, defaultKey, allowLiterals);
//	}

//	public int getIntAttribute(String name) {
//		return this.getIntAttribute(name, 0);
//	}
//
//	public int getIntAttribute(String name, int defaultValue) {
//		if (this.ignoreCase) {
//			name = name.toUpperCase();
//		}
//		String value = (String) this.attributes.get(name);
//		if (value == null) {
//			return defaultValue;
//		} else {
//			try {
//				return Integer.parseInt(value);
//			} catch (NumberFormatException e) {
//				throw this.invalidValue(name, value);
//			}
//		}
//	}

//	public int getIntAttribute(String name, HashMap valueSet, String defaultKey, boolean allowLiteralNumbers) {
//		if (this.ignoreCase) {
//			name = name.toUpperCase();
//		}
//		Object key = this.attributes.get(name);
//		Integer result;
//		if (key == null) {
//			key = defaultKey;
//		}
//		try {
//			result = (Integer) valueSet.get(key);
//		} catch (ClassCastException e) {
//			throw this.invalidValueSet(name);
//		}
//		if (result == null) {
//			if (!allowLiteralNumbers) {
//				throw this.invalidValue(name, (String) key);
//			}
//			try {
//				result = Integer.valueOf((String) key);
//			} catch (NumberFormatException e) {
//				throw this.invalidValue(name, (String) key);
//			}
//		}
//		return result.intValue();
//	}

//	public double getDoubleAttribute(String name) {
//		return this.getDoubleAttribute(name, 0.);
//	}

//	public double getDoubleAttribute(String name, double defaultValue) {
//		if (this.ignoreCase) {
//			name = name.toUpperCase();
//		}
//		String value = (String) this.attributes.get(name);
//		if (value == null) {
//			return defaultValue;
//		} else {
//			try {
//				return Double.valueOf(value).doubleValue();
//			} catch (NumberFormatException e) {
//				throw this.invalidValue(name, value);
//			}
//		}
//	}

//	public double getDoubleAttribute(String name, HashMap valueSet, String defaultKey, boolean allowLiteralNumbers) {
//		if (this.ignoreCase) {
//			name = name.toUpperCase();
//		}
//		Object key = this.attributes.get(name);
//		Double result;
//		if (key == null) {
//			key = defaultKey;
//		}
//		try {
//			result = (Double) valueSet.get(key);
//		} catch (ClassCastException e) {
//			throw this.invalidValueSet(name);
//		}
//		if (result == null) {
//			if (!allowLiteralNumbers) {
//				throw this.invalidValue(name, (String) key);
//			}
//			try {
//				result = Double.valueOf((String) key);
//			} catch (NumberFormatException e) {
//				throw this.invalidValue(name, (String) key);
//			}
//		}
//		return result.doubleValue();
//	}

//	public boolean getBooleanAttribute(String name, String trueValue, String falseValue, boolean defaultValue) {
//		if (this.ignoreCase) {
//			name = name.toUpperCase();
//		}
//		Object value = this.attributes.get(name);
//		if (value == null) {
//			return defaultValue;
//		} else if (value.equals(trueValue)) {
//			return true;
//		} else if (value.equals(falseValue)) {
//			return false;
//		} else {
//			throw this.invalidValue(name, (String) value);
//		}
//	}
	
//	private XMLException invalidValueSet(String name) {
//		String msg = "Invalid value set (entity name = \"" + name + "\")";
//		return new XMLException(this.getName(), msg);
//	}

//	private XMLException invalidValue(String name, String value) {
//		String msg = "Attribute \"" + name + "\" does not contain a valid " + "value (\"" + value + "\")";
//		return new XMLException(this.getName(), msg);
//	}

//	private XMLParseException unexpectedEndOfData() {
//		String msg = "Unexpected end of data reached";
//		return new XMLParseException(this.getName(), msg);
//	}

//	private XMLParseException syntaxError(String context) {
//		String msg = "Syntax error while parsing " + context;
//		return new XMLParseException(this.getName(), msg);
//	}
}
